ECS RunTaskで実行されるSpring Batchアプリケーションにおいて、特定のエラー条件時のみリトライさせてみる
ECS Runtask を定期実行する際、Step Functions を利用することで特定条件でリトライすることが可能です。
この際、Step Functions の再試行オプションを活用できます。
ただ、アプリケーションの終了コード等を使ってより柔軟なリトライを行いたいケースもあるかもしれません。
その場合は Lambda を活用したループ処理を実行する必要があり、aws-samples リポジトリでサンプル実装が公開されています。
今回は上記実装を Spring Batch の場合に置き換えつつ、試してみます。
構築内容
構成としては下図のような形にしました。
Step Functions 関連リソース以外に、Spring Batch のメタデータ格納用の Aurora PostgreSQL クラスターと定期実行用の EventBridge Scheduler を追加しています。
ワークフローとしては下記になります。
こちらは aws-samples のものと変わりません。
Lambda を用いて柔軟なリトライ判定を行いつつ、一定回数分繰り返します。
バッチ処理の内容としては、S3 からファイルをダウンロードするだけのシンプルなものにしています。
Step Functions から Runtask を呼び出す際に ContainerOverrides で CMD を指定しており、ここで実行ジョブ名を指定しています。
今回実行するジョブは 1 つだけなので意味が無いですが、このようにすることでコンテナやタスク定義をジョブ数分用意する必要がなくなります。
S3 に指定されたファイルが無い時は ABANDONED(6)で終了させ、それ以外の時は FAILED(5)で終了させます。
@Component("FileDownloadTasklet")
@StepScope
@Slf4j
public class FileDownloadTasklet implements Tasklet {
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
StepExecution stepExecution = chunkContext.getStepContext().getStepExecution();
S3Client s3Client = S3Client.builder()
.region(Region.AP_NORTHEAST_1)
.build();
String bucketName = System.getenv("BUCKET_NAME");
String fileKey = System.getenv("FILE_KEY");
try {
GetObjectRequest getObjectRequest = GetObjectRequest.builder()
.bucket(bucketName)
.key(fileKey)
.build();
s3Client.getObject(getObjectRequest);
} catch (S3Exception e) {
log.error(e.awsErrorDetails().errorMessage());
if (e.awsErrorDetails().errorCode().equals("NoSuchKey")) {
log.error("NoSuchKey");
stepExecution.setStatus(BatchStatus.ABANDONED);
} else {
stepExecution.setStatus(BatchStatus.FAILED);
}
}
return RepeatStatus.FINISHED;
}
}
Lambda の中で終了コードを確認して 6 の場合のみリトライを行います。
また、CannotPullContainerError と ResourceInitializationError の時もリトライを行います。
def is_retryable(cause):
# Retryable errors ECS returns
# https://docs.aws.amazon.com/AmazonECS/latest/userguide/stopped-task-error-codes.html
if cause['StoppedReason'].startswith("CannotPullContainerError"):
return True
if cause['StoppedReason'].startswith("ResourceInitializationError"):
return True
# Retryable errors your application returns
contianer = cause['Containers'][0]
if contianer.get('ExitCode', 0) in [6]:
return True
return False
CDK ベースで実装しており、コードは下記リポジトリに格納しているので、もしご興味があればご確認下さい。
試してみる
では、試してみます。
せっかく Event Bridge Scheduler を作ったのですが、検証するには面倒なので手動で Step Functions を起動します。
まず、ECR に指定したイメージが無い状態で起動してみます。
CannotPullContainerError 扱いになるので、3 回リトライを行いつつ、最終的に失敗扱いになります。
今度はイメージはあるものの、S3 に指定したファイルが無いパターンで試してみます。
この場合も、終了コードがが指定した値 (6) になるので 3 回ループして失敗扱いとなります。
次に S3 に途中でファイルを格納するパターンで試してみます。
バッチ処理に必要なファイルが遅れて格納されたイメージです。
リトライを経て、最終的には成功扱いになりました!
最初から S3 にファイルがあれば、もちろん 1 回目で成功します。
最後に S3 自体が無いパターンで試してみます。
特定条件に当てはまらないので、リトライもせず失敗になります。
まとめ
Step Functions 経由で ECS Runtask リトライ処理を試してみました。
Lambda を利用すれば ExitCode ごとに柔軟な処理を行う事が可能です。
このようなループ処理は直近追加された「変数」機能で楽に実装できそうなので、そちらも試してみようと思ってます。